﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using System.Net.Sockets;
using InterPC.SPF.SPFMechanisms;

/// 
/// Contains the main SPF Checker class
/// 

namespace InterPC.SPF {

    /// <summary>
    /// The main object for executing the SPF check
    /// </summary>
    public class SPFChecker {

        #region const

        /// <summary>
        /// Time to wait for the DNS server to respond
        /// </summary>
        const int DNS_CHECK_TIMEOUT = 5000;

        /// <summary>
        /// DNS Processing limit
        /// </summary>
        const int DNS_PROCESSING_LIMIT = 10;

        #endregion

        #region properties

        #region public

        private IPAddress _IPToCheck = null;
        /// <summary>
        /// Return the IP of the sender to be verified by the SPF policy if any
        /// This property is read-only, IP and sender must be passed in the constructor
        /// </summary>
        public IPAddress IPToCheck { get { return _IPToCheck; } }

        private string _SenderToCheck = null;
        /// <summary>
        /// Return the sender to check an SPF policy from. Contains a local and domain part
        /// (ex: eddy@interpc.fr, mail_sender.interpc.fr)
        /// This property is read-only, IP and sender must be passed in the constructor
        /// </summary>
        public string SenderToCheck { get { return _SenderToCheck; } }

        private OperationDetails _Details = new OperationDetails();
        /// <summary>
        /// Enable to set some extra information for the test that can be used / requiered 
        /// when arguments of the SPF policy are macro expended
        /// </summary>
        public OperationDetails Details {
            get { return _Details; }
            set { _Details = value; }
        }
       
        /// <summary>
        /// Indicate if log information must be added to the CheckLog property during check
        /// Default is false
        /// </summary>
        public bool EnableLogging { get; set; }

        private StringBuilder _CheckLog = new StringBuilder();
        /// <summary>
        /// Return log informations about the check_host opetation if EnableLogging has been set to true
        /// </summary>
        public string CheckLog {
            get { return _CheckLog.ToString(); }
        }

        private string _Explanation;
        /// <summary>
        /// Provide a quick explanation about the result of the check
        /// </summary>
        public string Explanation { get { return _Explanation; } }

        private bool _ExplanationByRemoteHost;
        /// <summary>
        /// Indicate if the explanation has been provided by remote checked host
        /// Value of false indicates that explanation is an internal quick explanation of the result
        /// Value of true indicates that explanation is provided by a macro expanded SPF explanation modifier 
        /// </summary>
        public bool ExplanationByRemoteHost {
            get { return _ExplanationByRemoteHost; }
        }

        private static IDNSProxy _DnsProxy = new DNSProxy();
        /// <summary>
        /// Get or set the IDNSProxy object that will be used for dns requests
        /// </summary>
        public static IDNSProxy DnsProxy {
            get { return _DnsProxy; }
            set { _DnsProxy = value; }
        }

        #endregion

        #region private

        /// <summary>
        /// Retrieve the local part of the sendertocheck property
        /// </summary>
        private string _SenderLocalPart {
            get {
                if (_SenderToCheck.Contains('@') && _SenderToCheck[0] != '@')
                    return _SenderToCheck.Split('@')[0];
                else
                    return "postmaster";
            }
        }
        /// <summary>
        /// Retrieve the domain part of the sendertocheck property
        /// </summary>
        private string _SenderDomain {
            get {
                if (_SenderToCheck.Contains('@'))
                    return _SenderToCheck.Split('@')[1];
                else
                    return _SenderToCheck;
            }
        }
        /// <summary>
        /// Retrieve the current time stamp
        /// </summary>
        private string CurrentTimeStamp {
            get { return DateTime.Now.ToString(); }
        }

        #endregion

        #endregion

        #region Fields

        /// <summary>
        /// Used to prevent Include mechanism and Redirect modifier from starting an infinite recursion
        /// </summary>
        internal Dictionary<string, SPFMechanismQualifier?> _CheckedDomains = new Dictionary<string, SPFMechanismQualifier?>();

        /// <summary>
        /// Used to provide tabbed indenting log during include mechanism
        /// </summary>
        internal int _RecursionDepth = 0;

        /// <summary>
        /// SPF implementations MUST limit the number of mechanisms and modifiers
        /// that do DNS lookups to at most 10 per SPF check 
        /// </summary>
        private int DNSlookupMechCount = 0;

        internal static Regex Reg_MacroRegEx = new Regex(@"%{1}\{(?<macro_letter>[a-z])(?<digits>[0-9]*)(?<transformer>r)?(?<delimiter>[.+,/_=-]?)\}", RegexOptions.IgnoreCase);
        internal static Regex Reg_MacroRegExStrict = new Regex(@"%{1}\{(?<macro_letter>[slodipvhcrt])(?<digits>[0-9]*)(?<transformer>r)?(?<delimiter>[.+,/_=-]?)\}", RegexOptions.IgnoreCase);
        internal static Regex Reg_MacroSyntaxError = new Regex(@"%[^{%_-]");

        #endregion

        #region constructors

        /// <summary>
        /// Main constructor
        /// </summary>
        /// <param name="ipToCheck">IP of the sender that must be submitted to the SPF policy</param>
        /// <param name="senderToCheck">Sender including local and domain part (ex: eddy@interpc.fr, mail_sender.interpc.fr). Domain is the host to search for SPF policy</param>
        public SPFChecker(IPAddress ipToCheck, string senderToCheck) {
            if (ipToCheck == null)
                throw new ArgumentNullException("ipToCheck");
            _IPToCheck = ipToCheck;
            // Convert to IPv4 if IPv4 mapped to IPv6
            if (_IPToCheck.IsIP4Mapped())
                _IPToCheck = _IPToCheck.UnmapIP4();
            _SenderToCheck = senderToCheck;
        }

        #endregion

        #region public methods

        /// <summary>
        /// Execute the check_host operation with the provided informations
        /// </summary>
        /// <returns>Result of the check</returns>
        public SPFResult Check_Host() {
            // Init log builder
            _CheckLog = new StringBuilder();
            // Log main informations
            Log("IP to check : {0}", _IPToCheck);
            Log("Domain to contact : {0}", _SenderDomain);
            // Launch the private method
            return Check_Host(_SenderDomain);
        }

        /// <summary>
        /// Return an macro expanded value of the given string regarding informations contained in this object
        /// </summary>
        /// <param name="domainSpec">macro to expand</param>
        /// <returns>The macro expanded string</returns>
        public string ExpandDomainSpec(string domainSpec) {
            return ExpandDomainSpec(_SenderDomain, domainSpec);
        }
        /// <summary>
        /// Return an macro expanded value of the given string regarding informations contained in this object
        /// </summary>
        /// <param name="domainSpec">macro to expand</param>
        /// <returns>The macro expanded string</returns>
        public string ExpandDomainSpec(string domainSpec, bool isExp) {
            return ExpandDomainSpec(_SenderDomain, domainSpec, isExp);
        }

        #endregion

        #region private methods

        /// <summary>
        /// Append a message to the log
        /// </summary>
        /// <param name="message">message to log</param>
        internal void Log(string message) {
            if (!this.EnableLogging)
                return;
            _CheckLog.Append("".PadLeft(_RecursionDepth, '\t'));
            _CheckLog.AppendLine(message); 
        }
        /// <summary>
        /// Append a message to the log with format
        /// </summary>
        /// <param name="message"></param>
        /// <param name="parameters"></param>
        internal void Log(string message, params object[] parameters) { Log(string.Format(message, parameters)); }

        /// <summary>
        /// Set explanation, log and return premError
        /// </summary>
        /// <param name="message">Explanation</param>
        /// <returns>SPFResult.PermError</returns>
        private SPFResult RaisePermError(string message) {
            _Explanation = message;
            Log("PermError : {0}", message);
            return SPFResult.PermError;
        }

        /// <summary>
        /// Set explanation with format, log and return premError
        /// </summary>
        /// <param name="message">Explanation</param>
        /// <returns>SPFResult.PermError</returns>
        private SPFResult RaisePermError(string message, params object[] parameters) {
            return RaisePermError(string.Format(message, parameters));
        }

        /// <summary>
        /// Set explanation, log and return tempError
        /// </summary>
        /// <param name="message">Explanation</param>
        /// <returns>SPFResult.PermError</returns>
        private SPFResult RaiseTempError(string message) {
            _Explanation = message;
            Log("TempError : {0}", message);
            return SPFResult.TempError;
        }
        /// <summary>
        /// Set explanation with format, log and return tempError
        /// </summary>
        /// <param name="message">Explanation</param>
        /// <returns>SPFResult.PermError</returns>
        private SPFResult RaiseTempError(string message, params object[] parameters) {
            return RaiseTempError(string.Format(message, parameters));
        }

        public bool IsValidDomainName(string domain, bool setExplanation) {
            if (Regex.IsMatch(domain, @"^\[[\.0-9]+\]$")) {
                if (setExplanation)
                    _Explanation = "domain name is not FQDN";
                return false;
            }
            string[] tab = domain.Split('.');
            if (tab.Length < 2) {
                if (setExplanation)
                    _Explanation = "domain name is not FQDN";
                return false;
            }
            foreach (string label in tab) {
                if (string.IsNullOrEmpty(label) && !label.EndsWith(".")) {
                    if (setExplanation)
                        _Explanation = "zero-length label other than ROOT";
                    return false;
                }
                if (label.Length > 63) {
                    if (setExplanation)
                        _Explanation = "domain label is longer than 63 chars";
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Main check_host function
        /// </summary>
        /// <param name="domain">Domain to obtain SPF policy from</param>
        /// <returns>SPFResult</returns> 
        internal SPFResult Check_Host(string domain) {

            // RFC 4.3 : domain name is well formed
            if (!IsValidDomainName(_SenderDomain, true)) {
                Log("Invalid domain name : {0}", _Explanation);
                return SPFResult.None;
            }                

            // RFC4408 : If both SPF and TXT RRs are
            // looked up, the queries MAY be done in parallel.
            Func<string, Heijden.DNS.Type, DnsQueryResult> DnsQueryDelegate = new Func<string, Heijden.DNS.Type, DnsQueryResult>(DoDnsQuery);
            DnsQueryResult SpfTypeQueryResult = new DnsQueryResult() { RCODE = -1 };
            DnsQueryResult TextTypeQueryResult = new DnsQueryResult() { RCODE = -1 };
            IAsyncResult SpfQueryAsyncResult = null;
            IAsyncResult TextQueryAsyncResult = null;
            SpfQueryAsyncResult = DnsQueryDelegate.BeginInvoke(domain, Heijden.DNS.Type.SPF, null, null);
            TextQueryAsyncResult = DnsQueryDelegate.BeginInvoke(domain, Heijden.DNS.Type.TXT, null, null);
            bool SpfQueryTimeOutIsOk = SpfQueryAsyncResult.AsyncWaitHandle.WaitOne(DNS_CHECK_TIMEOUT, false);
            if (SpfQueryTimeOutIsOk)
                SpfTypeQueryResult = DnsQueryDelegate.EndInvoke(SpfQueryAsyncResult);
            bool TextQueryTimeOutIsOk = TextQueryAsyncResult.AsyncWaitHandle.WaitOne(DNS_CHECK_TIMEOUT, false);
            if (TextQueryTimeOutIsOk)
                TextTypeQueryResult = DnsQueryDelegate.EndInvoke(TextQueryAsyncResult);

            // If all DNS lookups that are made return a server failure (RCODE 2),
            // or other error (RCODE other than 0 or 3), or time out, then
            // check_host() exits immediately with the result "TempError"
            if ((!SpfQueryTimeOutIsOk && !TextQueryTimeOutIsOk) || (SpfTypeQueryResult.RCODE != 0 && SpfTypeQueryResult.RCODE != 3 && TextTypeQueryResult.RCODE != 0 && TextTypeQueryResult.RCODE != 3)) {
                string message = "a dns error occurs while requiering SPF and TEXT records";
                if (!SpfQueryTimeOutIsOk)
                    message = "dns timeout";
                return RaiseTempError(message);
            }

            // if the DNS lookup returns "domain does not exist" (RCODE 3),
            // check_host() immediately returns the result "None".
            if (SpfTypeQueryResult.RCODE == 3 && TextTypeQueryResult.RCODE == 3) {
                _Explanation = "domain does not exist";
                Log(_Explanation);
                return SPFResult.None;
            }

            // Get SPF record
            string[] RecordsFound = GetSPFRecords(SpfTypeQueryResult.Records);
            Log("{0} SPF (99) RR Record found", RecordsFound.Length);
            // If any records of type SPF are in the set, then all records of
            // type TXT are discarded
            if (RecordsFound.Length == 0) {
                RecordsFound = GetSPFRecords(TextTypeQueryResult.Records);
                Log("{0} TEXT (16) RR Record found", RecordsFound.Length);
            }
                
            // If there are two or more records
            // remaining, then check_host() exits immediately with the result of
            // "PermError"
            if (RecordsFound.Length > 1)
                return RaisePermError("more than one SPF record has been found");
            // If no matching records are returned, an SPF client MUST assume that
            // the domain makes no SPF declarations.  SPF processing MUST stop and
            // return "None".
            if (RecordsFound.Length == 0) {
                _Explanation = "no SPF record found";
                return SPFResult.None;
            }
            string RecordToCheck = RecordsFound[0];
            Log("record found : {0}", RecordToCheck);

            // Implementations MAY choose to parse the entire record first and
            // return "PermError" if the record is not syntactically well formed.
            SPFRecord SPFRec = null;
            try {
                SPFRec = SPFRecord.Parse(RecordToCheck);
            }
            catch (FormatException ex) { return RaisePermError("error while parsing SPF record : {0}", ex.Message); }

            // Prevent infinite loops with include by maintening a list of checked domains
            _CheckedDomains[domain.ToLower().Trim()] = null;

            // Process terms
            string SPFRedirect = null;
            string SPFExp = null;

            // Process modifiers
            foreach (SPFModifierTerm Modif in SPFRec.Modifiers) {

                Log("analysing : {0}", Modif);

                switch (Modif.ModifierType) {
                    case SPFModifierType.Exp:
                        if (SPFExp != null)
                            return RaisePermError("more than one explanation term in SPF record");
                        SPFExp = Modif.Arg;
                        break;
                    case SPFModifierType.Redirect:
                        if (SPFRedirect != null)
                            return RaisePermError("more than one redirect term in SPF record");
                        SPFRedirect = Modif.Arg;
                        break;
                    case SPFModifierType.Unknown:
                        Log("unknown modifier : {0}", Modif);
                        break;
                }
            }

            // Process mechanisms
            foreach (SPFMechanismTerm Mech in SPFRec.Mechanisms) {

                Log("prossessing : {0}", Mech);

                // Check mechanism
                SPFMechanismQualifier R = SPFMechanismQualifier.Neutral;
                try {
                    // Processing limit
                    if (
                        new[] { SPFMechanismType.A, SPFMechanismType.MX, SPFMechanismType.PTR, SPFMechanismType.Include, SPFMechanismType.Exists }
                        .Contains(Mech.MechanismType)
                        )
                        DNSlookupMechCount++;
                    if (DNSlookupMechCount > DNS_PROCESSING_LIMIT)
                        return RaisePermError("Processing limit of {0} has been reached", DNS_PROCESSING_LIMIT);
                    // Perform the check
                    R = Mech.Check(this, _IPToCheck, domain);
                }
                catch (FormatException ex) {
                    return RaisePermError(ex.Message);
                }
                catch (PermErrorException ex) {
                    return RaisePermError(ex.Message);
                }
                catch (TempErrorException ex) {
                    return RaiseTempError(ex.Message);
                }

                // If match
                if (R != SPFMechanismQualifier.Neutral) {
                    _Explanation = string.Format("mechanism match (qualifier : {0})", R);
                    Log(_Explanation);

                    SPFResult Result = (SPFResult)(int)R;
                    // If explanation given, expand it
                    if (Result == SPFResult.Fail && SPFExp != null)
                        try {
                            ProcessExplanation(SPFExp);
                        }
                        catch (PermErrorException ex) {
                            return RaisePermError(ex.Message);
                        }
                    return Result;
                }
                else
                    Log("mechanism does not match");

            }

            // If none of the mechanisms match and there is no "redirect" modifier,
            // then the check_host() returns a result of "Neutral"
            if (SPFRedirect == null) {
                _Explanation = "no term match";
                Log(_Explanation);
                return SPFResult.Neutral;
            }
            else {
                // Process redirect
                string domain_to_redirect = ExpandDomainSpec(domain, SPFRedirect).Trim().ToLower();
                // Prévent redirect infinite loop
                if (_CheckedDomains.ContainsKey(domain_to_redirect)) {
                    return RaisePermError("Infinite recursion detected !");
                }
                Log("redirecting to : {0}", domain_to_redirect);
                Log("...");
                SPFResult R = this.Check_Host(domain_to_redirect);
                if (R == SPFResult.None)
                    return SPFResult.PermError;
                else
                    return R;
            }
        }

        internal string ExpandDomainSpec(string domain, string domainSpec) {
            return ExpandDomainSpec(domain, domainSpec, false);
        }

        internal string ExpandDomainSpec(string domain, string domainSpec, bool isExp) {

            string initialValue = domainSpec;
            string defaultValue = "unknown";

            if (!isExp)
                domainSpec = domainSpec.Trim('.');

            // A '%' character not followed by a '{', '%', '-', or '_' character is
            // a syntax error
            if (Reg_MacroSyntaxError.IsMatch(domainSpec))
                throw new PermErrorException("Syntax error in macro");

            // Find macros
            MatchCollection Matches = Reg_MacroRegEx.Matches(domainSpec);

            // Process all macros found
            foreach (Match M in Matches) {
                string macro_letter = M.Groups["macro_letter"].Value;
                string digits = M.Groups["digits"].Value;
                string transformer = M.Groups["transformer"].Value.ToLower();
                string delimiter = M.Groups["delimiter"].Value;

                bool IsUpperCaseMacro = macro_letter == macro_letter.ToUpper();
                macro_letter = macro_letter.ToLower();

                string source = "";
                switch (macro_letter) {
                    case "s":
                        source = _SenderToCheck;
                        break;
                    case "l":
                        source = _SenderLocalPart;
                        break;
                    case "o":
                        source = _SenderDomain;
                        break;
                    case "d":
                        source = domain;
                        break;
                    case "i":
                        source = _IPToCheck.ToString();
                        break;
                    case "p":
                        // Get PTR for <ip>
                        string[] PTR = null;
                        try {
                            PTR = DnsProxy.GetPTR(_IPToCheck);
                        }
                        catch (SocketException ex) { SPFMechanismTerm.HandleSocketException(ex); }
                        // Take the first validated name
                        foreach (string name in PTR) {
                            try {
                                IPAddress[] Adresses =
                                    _IPToCheck.AddressFamily == AddressFamily.InterNetworkV6 ?
                                    DnsProxy.GetAAAA(name) :
                                    DnsProxy.GetA(name);
                                if (Adresses.Contains(_IPToCheck)) {
                                    source = name;
                                    break;
                                }
                            }
                            catch (SocketException ex) { SPFMechanismTerm.HandleSocketException(ex); }
                        }
                        if (source == "")
                            source = defaultValue;
                        break;
                    case "v":
                        source = _IPToCheck.AddressFamily == AddressFamily.InterNetworkV6 ? "ip6" : "in-addr";
                        break;
                    case "h":
                        source = this.Details.HELODomain != null ? this.Details.HELODomain : defaultValue;
                        break;
                    case "c":
                        if (!isExp)
                            throw new PermErrorException("c macro letter is only allowed in exp datas");
                        source = _IPToCheck.ToString();
                        break;
                    case "r":
                        if (!isExp)
                            throw new PermErrorException("c macro letter is only allowed in exp datas");
                        source = this.Details.HostDomainName != null ? this.Details.HostDomainName : defaultValue;
                        break;
                    case "t":
                        if (!isExp)
                            throw new PermErrorException("c macro letter is only allowed in exp datas");
                        source = this.CurrentTimeStamp;
                        break;
                    default :
                        throw new FormatException("Unkown macro letter");
                }

                // Uppercased macros expand exactly as their lowercased equivalents, and
                // are then URL escaped.
                if (IsUpperCaseMacro)
                    source = Uri.EscapeDataString(source);
                
                // Delimiter ?
                if (delimiter != "") {
                    // convert
                    source = source.Replace(delimiter[0], '.');
                }

                if (transformer != "") {
                    // Reverse
                    // If ip, reverse bytes
                    try {
                        IPAddress Ip = IPAddress.Parse(source);
                        byte[] ip_bytes = Ip.GetAddressBytes();
                        ip_bytes = ip_bytes.Reverse().ToArray();
                        StringBuilder Sb = new StringBuilder();
                        foreach (byte b in ip_bytes) {
                            Sb.Append(
                                Ip.AddressFamily == AddressFamily.InterNetworkV6 ?
                                string.Format("{0}.{1}", Convert.ToString(b & 0xF, 16).ToUpper(), Convert.ToString(b >> 4, 16).ToUpper()) :
                                b.ToString());
                            Sb.Append(".");
                        }
                        source = Sb.ToString().Trim('.');
                    }
                    catch (FormatException) {
                        // Else reverse parts
                        string[] tab = source.Split('.');
                        Array.Reverse(tab);
                        source = string.Join(".", tab);
                    }
                }

                if (digits != "") {
                    // Keep n last parts
                    uint n = uint.Parse(digits);
                    string[] tab = source.Split('.');
                    if (tab.Length > n) {
                        string[] new_tab = new string[n];
                        Array.Copy(tab, tab.Length - (int)n, new_tab, 0, (int)n);
                        source = string.Join(".", new_tab);
                    }
                }

                // Replace
                domainSpec = domainSpec.Replace(M.Value, source);

                
            }
            
            if (domainSpec != initialValue)
                    Log("Expanded to : {0}", domainSpec);

            // A literal "%" is expressed by "%%"
            domainSpec = domainSpec.Replace("%%", "%");
            // "%_" expands to a single " " space.
            domainSpec = domainSpec.Replace("%_", " ");
            // "%-" expands to a URL-encoded space, viz., "%20".
            domainSpec = domainSpec.Replace("%-", "%20");

            return domainSpec;
        }

        internal string ExpandDomainSpecForQuery(string macro) {

            
            // Domain-spec must end in macro-expand or valid toplabel
            // Domain-spec may not consist of only a top-label without a leading dot
            if (!SPFChecker.Reg_MacroRegEx.IsMatch(macro)) {
                string[] tab = macro.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);

                // must have 2 parts
                if (tab.Length < 2)
                    throw new PermErrorException("Domain-spec must end in macro-expand or valid toplabel");

                // Verify top label
                string TopLabel = tab.Last();
                if (TopLabel.Contains(':') || // ':' char cannot appear in top label
                    TopLabel.StartsWith("-") || // TopLabel cannot begin with '-'
                    TopLabel.Contains('\0') || // Null not allowed in top-label
                    Regex.IsMatch(TopLabel, @"^[0-9]+$")) // Top label may not be all numeric
                    throw new PermErrorException("Domain-spec top label is invalid");
            }

            // Expend domain
            macro = ExpandDomainSpec(macro);

            // Domain name invalidity must be treated as inexistant
            if (!IsValidDomainName(macro, false))
                return "dummy_inexistant_hostname_InterPC.SPF.interpc.fr";

            // Trim if > 253
            while (macro.Length > 253) {
                string[] labels = macro.Split('.').Skip(1).ToArray();
                macro = string.Join(".", labels);
            }


            return macro;
        }

        /// <summary>
        /// Try to retrieve the explanation indicated by an explanation SPF modifier
        /// </summary>
        /// <param name="Explanation">Explanation modifier argument</param>
        private void ProcessExplanation(string Explanation) {
            string TargetName = null;

            try {
                TargetName = ExpandDomainSpecForQuery(Explanation);
            }
            catch (FormatException) { return; }

            string[] TXTRecords = null;
            try {
                TXTRecords = SPFChecker.DnsProxy.GetTXT(TargetName);
            }
            catch (SocketException) {
                return;
            }

            // Ignore exp if no TXT records. 
            // Ignore exp if multiple TXT records.
            if (TXTRecords.Length != 1)
                return;

            try {
                this._Explanation = ExpandDomainSpec(TXTRecords[0], true);
                _ExplanationByRemoteHost = true;
                Log("Remote explanation : {0}", _Explanation);
            }
            catch (FormatException) { }

            
        }

        /// <summary>
        /// Extract valid SPF record from a string array
        /// </summary>
        /// <param name="records">array of string to search in</param>
        /// <returns>an array of SPF records found</returns>
        private string[] GetSPFRecords(string[] records) {
            List<string> Result = new List<string>();
            if (records == null)
                return Result.ToArray();

            foreach (string R in records) {
                if (string.IsNullOrEmpty(R))
                    continue;
                // Records that do not begin with a version section of exactly
                // "v=spf1" are discarded.
                if (!Regex.IsMatch(R, "^v=spf1( |$)", RegexOptions.IgnoreCase))
                    continue;
                Result.Add(R);
            }

            return Result.ToArray();
        }

        /// <summary>
        /// Execute a DNS query
        /// </summary>
        /// <param name="ipOrHostName">ip or domain to query</param>
        /// <param name="Type">type of record to look for</param>
        /// <returns>a DnsQueryResult object</returns>
        private DnsQueryResult DoDnsQuery(string hostName, Heijden.DNS.Type type) {
            try {
                return 
                    new DnsQueryResult() { RCODE = 0,
                        Records =
                            type == Heijden.DNS.Type.TXT ?
                                DnsProxy.GetTXT(hostName) :
                                DnsProxy.GetSPF(hostName)
                    };
            }
            catch (SocketException ex) {

                // DNS RCODE error
                if (ex.ErrorCode / 100 == 90)
                    return new DnsQueryResult() { RCODE = ex.ErrorCode - 9000, Records = new string[0] };

                // Other socket error
                return new DnsQueryResult() { ErrorCode = ex.ErrorCode, Records = new string[0] };
            }
        }

        #endregion

        #region nested types

        /// <summary>
        /// Store details informations about the check context
        /// That could be used in SPF record macros
        /// </summary>
        public class OperationDetails {
            /// <summary>
            /// HELO/EHLO domain
            /// Can be used in macros
            /// </summary>
            public string HELODomain { get; set; }
            /// <summary>
            /// Domain name of host performing the check
            /// Can be used for remote explanation
            /// </summary>
            public string HostDomainName { get; set; }
        }
        
        /// <summary>
        /// Result of a dns query (include records found and returned code)
        /// </summary>
        class DnsQueryResult {
            public int RCODE { get; set; }
            public int ErrorCode { get; set; }
            public string[] Records { get; set; }
            public DnsQueryResult() {
                RCODE = -1;
                ErrorCode = -1;
            }
        }

        #endregion

    }

   

    

}
